package gitbucket.core.service

import java.nio.file.Files
import java.util.concurrent.ConcurrentHashMap
import gitbucket.core.model.Profile.profile.blockingApi.*
import gitbucket.core.model.activity.{CreateRepositoryInfo, ForkInfo}
import gitbucket.core.util.Directory.*
import gitbucket.core.util.{FileUtil, JGitUtil, LockUtil}
import gitbucket.core.model.{Account, Role}
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.servlet.Database
import org.apache.commons.io.FileUtils
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.dircache.DirCache
import org.eclipse.jgit.lib.{Constants, FileMode}

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.jdk.CollectionConverters._
import scala.util.Using

object RepositoryCreationService {

    private val Creating = new ConcurrentHashMap[String, Option[String]]()

    def isCreating(owner: String, repository: String): Boolean = {
        Option(Creating.get(s"${owner}/${repository}")).exists(_.isEmpty)
    }

    def startCreation(owner: String, repository: String): Unit = {
        Creating.put(s"${owner}/${repository}", None)
    }

    def endCreation(owner: String, repository: String, error: Option[String]): Unit = {
        error match {
            case None        => Creating.remove(s"${owner}/${repository}")
            case Some(error) => Creating.put(s"${owner}/${repository}", Some(error))
        }
    }

    def getCreationError(owner: String, repository: String): Option[String] = {
        Option(Creating.remove(s"${owner}/${repository}")).flatten
    }

}

trait RepositoryCreationService {
    self: AccountService
        with RepositoryService
        with LabelsService
        with WikiService
        with ActivityService
        with PrioritiesService =>

    def canCreateRepository(repositoryOwner: String, loginAccount: Account)(implicit
        session: Session
    ): Boolean = {
        repositoryOwner == loginAccount.userName || getGroupsByUserName(loginAccount.userName)
            .contains(repositoryOwner) || loginAccount.isAdmin
    }

    def createRepository(
        loginAccount: Account,
        owner: String,
        name: String,
        description: Option[String],
        isPrivate: Boolean,
        createReadme: Boolean,
        defaultBranch: String
    ): Future[Unit] = {
        createRepository(
          loginAccount,
          owner,
          name,
          description,
          isPrivate,
          if (createReadme) "README" else "EMPTY",
          None,
          defaultBranch
        )
    }

    def createRepository(
        loginAccount: Account,
        owner: String,
        name: String,
        description: Option[String],
        isPrivate: Boolean,
        initOption: String,
        sourceUrl: Option[String],
        defaultBranch: String
    ): Future[Unit] = Future {
        RepositoryCreationService.startCreation(owner, name)
        try {
            Database() withTransaction { implicit session =>
                // val ownerAccount = getAccountByUserName(owner).get
                val loginUserName = loginAccount.userName

                val copyRepositoryDir =
                    if (initOption == "COPY") {
                        sourceUrl.flatMap { url =>
                            val dir = Files.createTempDirectory(s"gitbucket-${owner}-${name}")
                                .toFile
                            Git.cloneRepository().setBare(true).setURI(url).setDirectory(dir)
                                .setCloneAllBranches(true).call()
                            Some(dir)
                        }
                    } else None

                // Insert to the database at first
                insertRepository(name, owner, description, isPrivate, defaultBranch)

                //    // Add collaborators for group repository
                //    if(ownerAccount.isGroupAccount){
                //      getGroupMembers(owner).foreach { member =>
                //        addCollaborator(owner, name, member.userName)
                //      }
                //    }

                // Insert default labels
                insertDefaultLabels(owner, name)

                // Insert default priorities
                insertDefaultPriorities(owner, name)

                // Create the actual repository
                val gitdir = getRepositoryDir(owner, name)
                JGitUtil.initRepository(gitdir, defaultBranch)

                if (initOption == "README" || initOption == "EMPTY_COMMIT") {
                    Using.resource(Git.open(gitdir)) { git =>
                        val builder = DirCache.newInCore.builder()
                        val inserter = git.getRepository.newObjectInserter()
                        val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")

                        if (initOption == "README") {
                            val content =
                                if (description.nonEmpty) {
                                    name + "\n" + "===============\n" + "\n" + description.get
                                } else { name + "\n" + "===============\n" }

                            builder.add(JGitUtil.createDirCacheEntry(
                              "README.md",
                              FileMode.REGULAR_FILE,
                              inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))
                            ))
                        }
                        builder.finish()

                        JGitUtil.createNewCommit(
                          git,
                          inserter,
                          headId,
                          builder.getDirCache.writeTree(inserter),
                          Constants.HEAD,
                          loginAccount.fullName,
                          loginAccount.mailAddress,
                          "Initial commit"
                        )
                    }
                }

                copyRepositoryDir.foreach { dir =>
                    try {
                        Using.resource(Git.open(dir)) { git =>
                            git.push().setRemote(gitdir.toURI.toString).setPushAll().setPushTags()
                                .call()
                            // Adjust the default branch
                            val branches = git.branchList().call().asScala
                                .map(_.getName.stripPrefix("refs/heads/"))
                            if (!branches.contains(defaultBranch)) {
                                val defaultBranch = Seq("master", "main").find(branches.contains)
                                    .getOrElse(branches.head)
                                saveRepositoryDefaultBranch(owner, name, defaultBranch)
                                // Change repository HEAD
                                Using.resource(Git.open(getRepositoryDir(owner, name))) { git =>
                                    git.getRepository.updateRef(Constants.HEAD, true)
                                        .link(Constants.R_HEADS + defaultBranch)
                                }
                            }
                        }
                    } finally { FileUtils.deleteQuietly(dir) }
                }

                // Create Wiki repository
                createWikiRepository(loginAccount, owner, name, defaultBranch)

                // Record activity
                recordActivity(CreateRepositoryInfo(owner, name, loginUserName))

                // Call hooks
                PluginRegistry().getRepositoryHooks.foreach(_.created(owner, name))
            }

            RepositoryCreationService.endCreation(owner, name, None)

        } catch {
            case ex: Exception => RepositoryCreationService
                    .endCreation(owner, name, Some(ex.toString))
        }
    }

    def forkRepository(
        accountName: String,
        repository: RepositoryInfo,
        loginUserName: String
    ): Future[Unit] = Future {
        RepositoryCreationService.startCreation(accountName, repository.name)
        try {
            LockUtil.lock(s"${accountName}/${repository.name}") {
                Database() withTransaction { implicit session =>
                    val originUserName = repository.repository.originUserName
                        .getOrElse(repository.owner)
                    val originRepositoryName = repository.repository.originRepositoryName
                        .getOrElse(repository.name)
                    val originDefaultBranchName = repository.repository.defaultBranch

                    insertRepository(
                      repositoryName = repository.name,
                      userName = accountName,
                      description = repository.repository.description,
                      isPrivate = repository.repository.isPrivate,
                      defaultBranch = originDefaultBranchName,
                      originRepositoryName = Some(originRepositoryName),
                      originUserName = Some(originUserName),
                      parentRepositoryName = Some(repository.name),
                      parentUserName = Some(repository.owner)
                    )

                    // Set default collaborators for the private fork
                    if (repository.repository.isPrivate) {
                        // Copy collaborators from the source repository
                        getCollaborators(repository.owner, repository.name)
                            .foreach { case (collaborator, _) =>
                                addCollaborator(
                                  accountName,
                                  repository.name,
                                  collaborator.collaboratorName,
                                  collaborator.role
                                )
                            }
                        // Register an owner of the source repository as a collaborator
                        addCollaborator(
                          accountName,
                          repository.name,
                          repository.owner,
                          Role.ADMIN.name
                        )
                    }

                    // Insert default labels
                    insertDefaultLabels(accountName, repository.name)
                    // Insert default priorities
                    insertDefaultPriorities(accountName, repository.name)

                    // clone repository actually
                    JGitUtil.cloneRepository(
                      getRepositoryDir(repository.owner, repository.name),
                      FileUtil.deleteIfExists(getRepositoryDir(accountName, repository.name))
                    )

                    // Create Wiki repository
                    JGitUtil.cloneRepository(
                      getWikiRepositoryDir(repository.owner, repository.name),
                      FileUtil.deleteIfExists(getWikiRepositoryDir(accountName, repository.name))
                    )

                    // Copy LFS files
                    val lfsDir = getLfsDir(repository.owner, repository.name)
                    if (lfsDir.exists) {
                        FileUtils.copyDirectory(
                          lfsDir,
                          FileUtil.deleteIfExists(getLfsDir(accountName, repository.name))
                        )
                    }

                    // Record activity
                    val forkInfo =
                        ForkInfo(repository.owner, repository.name, loginUserName, accountName)
                    recordActivity(forkInfo)

                    // Call hooks
                    PluginRegistry().getRepositoryHooks
                        .foreach(_.forked(repository.owner, accountName, repository.name))

                    RepositoryCreationService.endCreation(accountName, repository.name, None)
                }
            }
        } catch {
            case ex: Exception => RepositoryCreationService
                    .endCreation(accountName, repository.name, Some(ex.toString))
        }
    }

    def insertDefaultLabels(userName: String, repositoryName: String)(implicit s: Session): Unit = {
        createLabel(userName, repositoryName, "bug", "fc2929")
        createLabel(userName, repositoryName, "duplicate", "cccccc")
        createLabel(userName, repositoryName, "enhancement", "84b6eb")
        createLabel(userName, repositoryName, "invalid", "e6e6e6")
        createLabel(userName, repositoryName, "question", "cc317c")
        createLabel(userName, repositoryName, "wontfix", "ffffff")
    }

    def insertDefaultPriorities(userName: String, repositoryName: String)(implicit
        s: Session
    ): Unit = {
        createPriority(
          userName,
          repositoryName,
          "highest",
          Some(
            "All defects at this priority must be fixed before any public product is delivered."
          ),
          "fc2929"
        )
        createPriority(
          userName,
          repositoryName,
          "very high",
          Some("Issues must be addressed before a final product is delivered."),
          "fc5629"
        )
        createPriority(
          userName,
          repositoryName,
          "high",
          Some(
            "Issues should be addressed before a final product is delivered. If the issue cannot be resolved before delivery, it should be prioritized for the next release."
          ),
          "fc9629"
        )
        createPriority(
          userName,
          repositoryName,
          "important",
          Some(
            "Issues can be shipped with a final product, but should be reviewed before the next release."
          ),
          "fccd29"
        )
        createPriority(userName, repositoryName, "default", Some("Default."), "acacac")

        setDefaultPriority(
          userName,
          repositoryName,
          getPriority(userName, repositoryName, "default").map(_.priorityId)
        )
    }
}
